第15章 初始化
整个初始化过程相当烦琐,要完成诸如命令行参数整理、环境变量设置,以及内存分配器、垃圾回收器和并发调度器的工作现场准备。
依照第14章找出的线索,先依次看看几个初始化函数的内容。依旧用设置断点命令确定函数所在的源文件名和代码行号。
(gdb)b runtime.args Breakpoint 7 at 0x42ebf0:file/usr/local/go/src/runtime/runtime1.go,line 48.
(gdb)b runtime.osinit Breakpoint 8 at 0x41e9d0:file/usr/local/go/src/runtime/os1_linux.go,line 172.
(gdb)b runtime.schedinit Breakpoint 9 at 0x424590:file/usr/local/go/src/runtime/proc1.go,line 40.
函数args整理命令行参数,这个没什么需要深究的。
runtime1.go
func args(c int32,v**byte) { argc=c argv=v sysargs(c,v) }
函数osinit确定CPU Core数量。
os1_linux.go
func osinit() { ncpu=getproccount() }
最关键的就是schedinit这里,几乎我们要关注的所有运行时环境初始化构造都是在这里被调用的。函数头部的注释列举了启动过程,也就是第14章的内容,不过信息太过简洁了。
proc1.go
//The bootstrap sequence is: // // call osinit // call schedinit // make&queue new G // call runtime·mstart func schedinit() { // 最大系统线程数量限制,参考标准库runtime/debug.SetMaxThreads sched.maxmcount=10000
// 栈、内存分配器、调度器相关初始化 stackinit() mallocinit() mcommoninit(g.m)
// 处理命令行参数和环境变量 goargs() goenvs()
// 处理GODEBUG、GOTRACEBACK调试相关的环境变量设置 parsedebugvars()
// 垃圾回收器初始化 gcinit()
// 通过CPU Core和GOMAXPROCS环境变量确定P数量 procs:=int(ncpu) if n:=atoi(gogetenv(“GOMAXPROCS”));n>0{ if n> _MaxGomaxprocs{ n= _MaxGomaxprocs } procs=n }
// 调整P数量 if procresize(int32(procs)) !=nil{ throw(“unknown runnable goroutine during bootstrap”) } }
内存分配器、垃圾回收器、并发调度器的初始化细节需要涉及很多专属特征,先不去理会,留待后续章节再做详解。
事实上,初始化操作到此并未结束,因为接下来要执行的是runtime.main,而不是用户逻辑入口函数main.main。
(gdb)b runtime.main Breakpoint 10 at 0x423250:file/usr/local/go/src/runtime/proc.go,line 28.
在这里我们关注的焦点是:包初始化函数init的执行。
proc.go
//The main goroutine. func main() { // 执行栈的最大限制:1 GB on 64-bit,250 MB on 32-bit. if ptrSize==8{ maxstacksize=1000000000 }else{ maxstacksize=250000000 }
…
// 启动系统后台监控(定期垃圾回收,以及并发任务调度相关的信息) systemstack(func() { newm(sysmon,nil) })
…
// 执行runtime包内所有初始化函数init runtime_init()
…
// 启动垃圾回收器后台操作 gcenable()
// 执行所有的用户包(包括标准库)初始化函数init main_init()
…
// 执行用户逻辑入口main.main函数 main_main()
// 执行结束,返回退出状态码 exit(0) }
与之相关的就是runtime_init和main_init这两个函数,它们都由编译器动态生成。
proc.go
//go:linkname runtime_init runtime.init func runtime_init()
//go:linkname main_init main.init func main_init()
//go:linkname main_main main.main func main_main()
注意链接后符号名的变化:runtime_init > runtime.init。
我们准备一个稍微复杂点的示例,看看编译器究竟干了什么。
-
| +-main.go,test.go | +- | +-sum.go
lib/sum.go
package lib
func init() { println(“sum.init”) }
func Sum(x…int)int{ n:=0 for_,i:=range x{ n+=i }
return n }
test.go
package main
import( “lib” )
func init() { println(“test.init”) }
func test() { println(lib.Sum(1,2,3)) }
main.go
package main
import( _ “net/http” // 引入一个标准库里的包 )
func init() { println(“main.init.2”) }
func main() { test() }
func init() { println(“main.init.1”) }
编译,执行输出:
$go build-gcflags”-N-l” -o test
$ ./test sum.init main.init.2 main.init.1 test.init 6
接下来我们用反汇编工具,看看最终动态生成代码的真实面目。
$go tool objdump-s”runtime.init\b”test
TEXT runtime.init.1(SB) /usr/local/go/src/runtime/alg.go alg.go:322 …
TEXT runtime.init.2(SB) /usr/local/go/src/runtime/mstats.go mstats.go:148 …
TEXT runtime.init.3(SB) /usr/local/go/src/runtime/panic.go panic.go:154 …
TEXT runtime.init.4(SB) /usr/local/go/src/runtime/proc.go proc.go:140 …
TEXT runtime.init(SB) /usr/local/go/src/runtime/zversion.go zversion.go:9 … panic.go:9 … select.go:45 …
zversion.go:9 CALL runtime.init.1(SB) zversion.go:9 CALL runtime.init.2(SB) zversion.go:9 CALL runtime.init.3(SB) zversion.go:9 CALL runtime.init.4(SB) zversion.go:9 MOVL0x58,SP zversion.go:9 RET
命令行工具go tool objdump可用来查看实际生成的汇编代码,参数使用正则表达式。当然如果习惯Intel格式,那么还是用GDB吧。
很显然,runtime内相关的多个init函数被赋予唯一符号名,然后再由runtime.init进行统一调用。注意,zversion.go也是动态生成的。
zversion.go
//auto generated by go tool dist
package runtime
const defaultGoroot= /usr/local/go
const theVersion= go1.5.1
const goexperiment= “
const stackGuardMultiplier=1
var buildVersion=theVersion
至于main.init,情况基本一致。区别在于它负责调用非runtime包的初始化函数。
$go tool objdump-s”main.init\b”test
TEXT main.init.1(SB)src/main.go main.go:7 …
TEXT main.init.2(SB)src/main.go main.go:15 …
TEXT main.init.3(SB)src/test.go test.go:7 …
TEXT main.init(SB)src/test.go test.go:13 … test.go:13 CALL net/http.init(SB) test.go:13 CALL test/lib.init(SB) test.go:13 CALL main.init.1(SB) test.go:13 CALL main.init.2(SB) test.go:13 CALL main.init.3(SB) test.go:13 MOVL$0x2,0x48d543(IP) test.go:13 RET
被引用的包,包括lib和标准库net/http里的init函数,都被main.init调用。
虽然从当前版本的编译器角度来说,init的执行顺序和依赖关系、文件名,以及定义顺序有关。但这种次序非常不便于维护和理解,极易造成潜在错误,所以强烈建议让init只做该做的事情:局部初始化。
最后需要记住:
- 所有init函数都在同一个goroutine内执行。
- 所有init函数结束后才会执行main.main函数。